<!--
  Floorplan for Home Assistant
  Version: 1.0.13
  https://github.com/pkozul/ha-floorplan
-->

<script src="lib/jquery-3.2.1.min.js"></script>
<script src="lib/moment.min.js"></script>
<script src="lib/svg-pan-zoom.min.js"></script>

<!-- As documented here for chrome, removes the need for touchstart -->
<meta name="viewport" content="width=device-width">

<dom-module id="ha-floorplan">

  <template>
    <style>
      .loading-container {
        text-align: center;
        padding: 8px;
      }

      .loading {
        height: 0px;
        overflow: hidden;
      }

      #errors {
        color: #FF0000;
        display: none;
      }

      #warnings {
        color: #FF851B;
        display: none;
      }

      #debug {
        color: #000000;
        display: none;
      }
    </style>

    <template is='dom-if' if='[[isLoading]]'>
      <div class='loading-container'>
        <paper-spinner active alt='Loading'></paper-spinner>
      </div>
    </template>

    <div id="errors">
      <ul></ul>
    </div>

    <div id="warnings">
      <ul></ul>
    </div>

    <div id="debug">
      <ul></ul>
    </div>

    <div id="floorplan" on-tap="stopPropagation"></div>

  </template>

</dom-module>

<script>

  class HaFloorplan extends Polymer.Element {
    static get is() { return 'ha-floorplan'; }

    static get properties() {
      return {
        hass: {
          type: Object,
          observer: 'hassChanged'
        },
        inDialog: {
          type: Boolean,
          value: false,
        },
        isPanel: {
          type: Boolean,
          value: false,
        },
        config: {
          type: Object,
        },
        isLoading: {
          type: Boolean,
          value: true,
        },
        timeDifference: {
          type: Number,
          value: undefined,
        },
        entityConfigs: {
          type: Array,
          value: () => { return []; },
        },
        elementConfigs: {
          type: Array,
          value: () => { return []; },
        },
        cssRules: {
          type: Array,
          value: () => { return []; },
        },
        isInitialized: {
          type: Boolean,
          value: false,
        },
      };
    }

    connectedCallback() {
      super.connectedCallback();

      if (!this.isInitialized) {
        this.initFloorplan();
      }
    }

    stopPropagation(e) {
      e.stopPropagation();
    }

    hassChanged(newHass, oldHass) {
      this.handleEntities(newHass.states);
    }

    initFloorplan() {
      this.isInitialized = true;

      window.onerror = this.handleWindowError.bind(this);

      if (!this.config.groups) {
        this.isLoading = false;
        this.warn(`Cannot find 'groups' in floorplan configuration`);
        return;
      }

      let invalidGroups = this.config.groups.filter(x => x.entities && x.elements);
      if (invalidGroups.length) {
        this.isLoading = false;
        this.warn(`A group cannot contain both 'entities' and 'elements' in floorplan configuration`);
        return;
      }

      invalidGroups = this.config.groups.filter(x => !x.entities && !x.elements);
      if (invalidGroups.length) {
        this.isLoading = false;
        this.warn(`A group must contain either 'entities' or 'elements' in floorplan configuration`);
        return;
      }

      this.hass.connection.socket.addEventListener('message', event => {
        let data = JSON.parse(event.data);

        // Store the time difference between the local web browser and the Home Assistant server
        if (data.event && data.event.time_fired) {
          let lastEventFiredTime = moment(data.event.time_fired).toDate();
          this.timeDifference = moment().diff(moment(lastEventFiredTime), 'milliseconds');
        }
      });

      this.addExternalCss(() => {
        this.loadFloorPlan(() => {
          this.isLoading = false;
          this.handleEntities(this.hass.states);
        });
      });

      if (this.config.groups.find(entityGroup => entityGroup.state_transitions)) {
        setInterval(this.updateStateTransitions.bind(this), 100);
      }
    }

    handleWindowError(msg, url, lineNo, columnNo, error) {
      if (msg.toLowerCase().indexOf("script error") >= 0) {
        this.error('Script error: See browser console for detail');
      }
      else {
        let message = [
          msg,
          'URL: ' + url,
          'Line: ' + lineNo + ', column: ' + columnNo,
          'Error: ' + JSON.stringify(error)
        ].join('<br>');

        this.error(message);
      }

      return false;
    }

    addExternalCss(callback) {
      if (!this.config.stylesheet) {
        callback();
      }

      this.loadStyleSheet(this.config.stylesheet + '?cacheBuster=' + (new Date().getTime()), function (success, link) {
        if (success) {
          this.instance.root.appendChild(link);
          let styleSheet = link['sheet'];
          setTimeout(() => {
            this.instance.cssRules = this.instance.getArray(styleSheet.cssRules);
            callback();
          }, 1000);
        }
        else {
          this.instance.error("Error loading stylesheet");
        }
      }.bind({ instance: this, callback: callback }));
    }

    loadFloorPlan(callback) {
      jQuery.ajax({
        url: this.config.image + '?cacheBuster=' + (new Date().getTime()),
        success: function (result) {
          let svg = $(result).find('svg')[0];

          $(svg).height('100%');
          $(svg).width('100%');
          $(svg).css('position', this.instance.isPanel ? 'absolute' : 'relative');
          $(svg).css('cursor', 'default');

          this.instance.$.floorplan.appendChild(svg);

          let uniqueId = (new Date()).getTime();

          let svgElements = $(svg).find('*').toArray();

          let elementGroups = this.instance.config.groups.filter(x => x.elements);
          for (let elementGroup of elementGroups) {
            for (let elementId of elementGroup.elements) {
              let svgElement = $(svg).find(`[id="${elementId}"]`);

              if (svgElement.length) {
                $(svgElement).on('click', this.instance.onElementClick.bind({ instance: this.instance, elementId: elementId }));
                $(svgElement).css('cursor', 'pointer');

                let elementConfig = {
                  group: elementGroup,
                };

                this.instance.elementConfigs[elementId] = elementConfig;

                if (elementGroup.action.data.elements) {
                  for (let otherElementId of elementGroup.action.data.elements) {
                    let otherSvgElement = $(svg).find(`[id="${otherElementId}"]`);
                    $(otherSvgElement).addClass(elementGroup.action.data.default_class);
                  }
                }
              }
              else {
                this.instance.warn(`Cannot find '${elementId}' in SVG file`);
              }
            }
          }

          let entityGroups = this.instance.config.groups.filter(x => x.entities);
          for (let entityGroup of entityGroups) {
            let targetEntityIds = [];

            // Split out HA entity groups into separate entities
            if (entityGroup.groups) {
              for (let entityId of entityGroup.groups) {
                let group = this.instance.hass.states[entityId];
                if (group) {
                  for (let targetEntityId of group.attributes.entity_id) {
                    targetEntityIds.push(targetEntityId);
                  }
                }
                else {
                  this.instance.warn(`Cannot find '${entityId}' in HA group configuration`);
                }
              }
            }

            // HA entities treated as is
            if (entityGroup.entities) {
              for (let entityId of entityGroup.entities) {
                let entity = this.instance.hass.states[entityId];
                if (entity) {
                  targetEntityIds.push(entityId);
                }
                else {
                  this.instance.warn(`Cannot find '${entityId}' in HA group configuration`);
                }
              }
            }

            for (let entityId of targetEntityIds) {
              let entityConfig = {
                group: entityGroup,
                lastState: undefined,
                lastChangedTime: undefined,
                svgElementConfigs: {},
                imageUrl: undefined
              };

              this.instance.entityConfigs[entityId] = entityConfig;

              let svgElement = svgElements.find(svgElement => svgElement.id === entityId);
              if (!svgElement) {
                this.instance.warn(`Cannot find element '${entityId}' in SVG file`);
                continue;
              }

              entityConfig.svgElementConfigs[svgElement.id] = {
                svgElementId: svgElement.id,
                svgElement: svgElement,
                clonedsvgElement: svgElement.cloneNode(true),
                entityId: entityId
              };

              $(svgElement).find('*').each((i, svgNestedElement) => {
                // Ensure that all child elements have an Id.
                if (!svgNestedElement.id) {
                  svgNestedElement.id = uniqueId++;
                }

                entityConfig.svgElementConfigs[svgNestedElement.id] = {
                  svgElementId: svgNestedElement.id,
                  svgElement: svgNestedElement,
                  clonedsvgElement: svgNestedElement.cloneNode(true),
                  entityId: entityId
                };
              });

              for (let svgElementId in entityConfig.svgElementConfigs) {
                let svgElementConfig = entityConfig.svgElementConfigs[svgElementId];

                let svgElement = $(svgElementConfig.svgElement);

                // Create a title element (to support hover over text)
                svgElement.append(document.createElementNS('http://www.w3.org/2000/svg', 'title'));

                if (svgElement.length) {
                  svgElementConfig.svgElement = svgElement[0];

                  $(svgElement).on('click', this.instance.onEntityClick.bind({ instance: this.instance, entityId: entityId }));
                  $(svgElement).css('cursor', 'pointer');
                  $(svgElement).addClass('ha-entity');

                  if ((svgElement[0].nodeName === 'text') && (svgElement[0].id === entityId)) {
                    let boundingBox = svgElement[0].getBBox();
                    let rect = $(document.createElementNS("http://www.w3.org/2000/svg", 'rect'))
                      .attr('id', entityId + '.background')
                      .attr('height', boundingBox.height + 1)
                      .attr('width', boundingBox.width + 2)
                      .height(boundingBox.height + 1)
                      .width(boundingBox.width + 2)
                      .attr('x', boundingBox.x - 1)
                      .attr('y', boundingBox.y - 0.5)
                      .css('fill-opacity', 0);

                    $(rect).insertBefore($(svgElement));
                  }
                }
              }
            }
          }

          // Enable pan / zoom if enabled in config
          if ((this.instance.config.pan_zoom === null) || (this.instance.config.pan_zoom !== undefined)) {
            svgPanZoom($(svg)[0], {
              zoomEnabled: true,
              controlIconsEnabled: true,
              fit: true,
              center: true,
            });
          }

          this.callback();

        }.bind({ instance: this, callback: callback })
      });
    }

    handleEntities(entities) {
      let svg = this.$.floorplan.querySelector('svg');

      for (let entityId in entities) {
        let entityState = entities[entityId];

        let entityConfig = this.entityConfigs[entityId];
        if (!entityConfig)
          continue;

        entityConfig.lastState = entityState.state;

        for (let svgElementId in entityConfig.svgElementConfigs) {
          let svgElementConfig = entityConfig.svgElementConfigs[svgElementId];
          let svgElement = svgElementConfig.svgElement;

          if (!svgElement)
            continue;

          this.setHoverOverText(svgElement, entityState);

          if (svgElement.nodeName === 'text') {
            let text = entityConfig.group.text_template ?
              this.assemble(entityConfig.group.text_template, entityState, entities) : entityState.state;

            let tspan = $(svgElement).find('tspan');
            if (tspan.length) {
              $(tspan).text(text);
            }
            else {
              let title = $(svgElement).find('title');
              $(svgElement).text(text);
              if (title.length) {
                $(svgElement).append(title);
              }
            }

            let rect = $(svgElement).parent().find(`[id="${entityId}.background"]`);
            if (rect.length) {
              let boundingBox = svgElement.getBBox();
              $(rect)
                .attr("x", boundingBox.x - 1)
                .attr("y", boundingBox.y - 0.5)
                .attr('height', boundingBox.height + 1)
                .attr('width', boundingBox.width + 2)
                .height(boundingBox.height + 1)
                .width(boundingBox.width + 2);
            }
          }

          if (!this.cssRules || !this.cssRules.length)
            return;

          let wasTransitionHandled = false;

          if (entityConfig.group.states && entityConfig.group.state_transitions) {
            let transitionConfig = entityConfig.group.state_transitions.find(transitionConfig => (transitionConfig.to_state === entityState.state));
            if (transitionConfig && transitionConfig.from_state && transitionConfig.to_state && transitionConfig.duration) {
              // Determine the current time on the server (based on the local vs. server time difference)
              let serverMoment = this.getServerMoment();
              let lastChangedMoment = moment(entityState.last_changed);
              let elapsed = Math.max(serverMoment.diff(lastChangedMoment, 'milliseconds'), 0);
              let remaining = (transitionConfig.duration * 1000) - elapsed;

              if (remaining > 0) {
                entityConfig.lastChangedTime = lastChangedMoment.toDate();
              }
              else {
                this.setEntityStyle(svgElementConfig, svgElement, entityConfig);
              }
              wasTransitionHandled = true;
            }
          }

          if (entityConfig.group.image_template) {
            let imageUrl = this.assemble(entityConfig.group.image_template, entityState, entities);
            if (entityConfig.imageUrl !== imageUrl) {
              entityConfig.imageUrl = imageUrl;
              this.loadImage(imageUrl, entityId, entityState, (embeddedSvg, entityState) => {
                this.setHoverOverText(embeddedSvg, entityState);
              });
            }

            let embeddedSvg = $(svg).find(`[id="image.${entityId}"]`)[0];
            this.setHoverOverText(embeddedSvg, entityState);
          }

          let targetClass = undefined;
          let obsoleteClasses = [];

          if (entityConfig.group.class_template) {
            targetClass = this.assemble(entityConfig.group.class_template, entityState, entities);
          }

          let originalClasses = this.getArray(svgElementConfig.clonedsvgElement.classList);

          // Get the config for the current state
          if (entityConfig.group.states) {
            let stateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === entityState.state));
            if (stateConfig && stateConfig.class && !wasTransitionHandled) {
              targetClass = stateConfig.class;
            }

            // Remove any other previously-added state classes
            for (let otherStateConfig of entityConfig.group.states) {
              if (!stateConfig || (otherStateConfig.state != stateConfig.state)) {
                if (otherStateConfig.class && (otherStateConfig.class != 'ha-entity') && $(svgElement).hasClass(otherStateConfig.class)) {
                  if (originalClasses.indexOf(otherStateConfig.class) < 0) {
                    obsoleteClasses.push(otherStateConfig.class);
                  }
                }
              }
            }
          }
          else {
            for (let otherClassName of this.getArray(svgElement.classList)) {
              if ((otherClassName != targetClass) && (otherClassName != 'ha-entity')) {
                if (originalClasses.indexOf(otherClassName) < 0) {
                  obsoleteClasses.push(otherClassName);
                }
              }
            }
          }

          // Remove any obsolete classes from the entity
          this.removeClasses(entityId, svgElement, obsoleteClasses);

          // Add the target class to the entity
          if (targetClass) {
            this.addClass(entityId, svgElement, targetClass);
          }

          if (this.config.last_motion_entity && this.config.last_motion_class && entities[this.config.last_motion_entity] &&
            (entityState.attributes.friendly_name === entities[this.config.last_motion_entity].state)) {
            if (!$(svgElement).hasClass(this.config.last_motion_class)) {
              $(svgElement).addClass(this.config.last_motion_class);
            }
          }
          else {
            if ($(svgElement).hasClass(this.config.last_motion_class)) {
              $(svgElement).removeClass(this.config.last_motion_class);
            }
          }
        }
      }
    }

    setHoverOverText(element, entityState) {
      let title = $(element).find('title');
      if (title.length) {
        let dateFormat = this.config.date_format ? this.config.date_format : 'DD-MMM-YYYY';
        let titleText = entityState.attributes.friendly_name + '\n' +
          'State: ' + entityState.state + '\n' +
          'Last changed date: ' + moment(entityState.last_changed).format(dateFormat) + '\n' +
          'Last changed time: ' + moment(entityState.last_changed).format('HH:mm:ss');

        $(title).html(titleText);
      }
    }

    loadImage(imageUrl, entityId, entityState, callback) {
      let svg = this.$.floorplan.querySelector('svg');
      jQuery.ajax({
        url: imageUrl, // allow the browser cache to be used
        success: function (result) {
          let svgElement = $(svg).find(`[id="${entityId}"]`);
          let bbox = svgElement[0].getBBox();
          let clientRect = svgElement[0].getBoundingClientRect();

          let embeddedSvg = $(result).find('svg');

          embeddedSvg.attr('id', `image.${entityId}`);
          embeddedSvg.attr('preserveAspectRatio', 'xMinYMin meet')
          embeddedSvg
            .attr('height', bbox.height)
            .attr('width', bbox.width)
            .attr('x', bbox.x)
            .attr('y', bbox.y);

          $(embeddedSvg).find('*').append(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
            .on('click', this.onEntityClick.bind({ instance: this, entityId: entityId }))
            .css('cursor', 'pointer')
            .addClass('ha-entity');

          // Remove previous SVG
          let previousEmbeddedSvg = $(svg).find(`[id="${embeddedSvg.attr('id')}"]`);
          $(previousEmbeddedSvg).find('*')
            .off('click')
            .remove();

          $(svg).append(embeddedSvg);

          callback(embeddedSvg, entityState);

        }.bind(this)
      });
    }

    addClass(entityId, svgElement, className) {
      if ($(svgElement).hasClass('ha-leave-me-alone')) {
        return;
      }

      if (!$(svgElement).hasClass(className)) {
        //console.log(`${entityId}: adding class "${className}" for current state "${entityState.state}" (${svgElement.id})`);
        $(svgElement).addClass(className);

        if ((svgElement.nodeName === 'text')) {
          let rect = $(svgElement).parent().find(`[id="${entityId}.background"]`);
          if (rect.length) {
            if (!$(rect).hasClass(className + '-background')) {
              $(rect).addClass(className + '-background');
            }
          }
        }
      }
    }

    removeClasses(entityId, svgElement, classes) {
      for (let className of classes) {
        //console.log(`${entityId}: removing class "${className}" (${svgElement.id})`);
        if ($(svgElement).hasClass(className)) {
          $(svgElement).removeClass(className);

          if ((svgElement.nodeName === 'text')) {
            let rect = $(svgElement).parent().find(`[id="${entityId}.background"]`);
            if (rect.length) {
              if ($(rect).hasClass(className + '-background')) {
                $(rect).removeClass(className + '-background');
              }
            }
          }
        }
      }
    }

    updateStateTransitions() {
      if (!this.cssRules || !this.cssRules.length)
        return;

      let svg = this.$.floorplan.querySelector('svg');

      for (let entityId in this.entityConfigs) {
        let entityConfig = this.entityConfigs[entityId];

        if (!entityConfig || !entityConfig.group.states || !entityConfig.group.state_transitions || (entityConfig.lastChangedTime === undefined))
          continue;

        for (let svgElementId in entityConfig.svgElementConfigs) {
          let svgElementConfig = entityConfig.svgElementConfigs[svgElementId];
          let svgElement = svgElementConfig.svgElement;

          if (!svgElement)
            continue;

          let wasTransitionHandled = false;

          let transitionConfig = entityConfig.group.state_transitions.find(transitionConfig => (transitionConfig.to_state === entityConfig.lastState));
          if (transitionConfig && transitionConfig.from_state && transitionConfig.to_state && transitionConfig.duration) {
            let serverMoment = this.getServerMoment();
            let fromStateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === transitionConfig.from_state));
            let toStateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === transitionConfig.to_state));

            if (fromStateConfig && toStateConfig) {
              let fromFill = this.getFill(fromStateConfig);
              let toFill = this.getFill(toStateConfig);

              if (fromFill && toFill) {
                let elapsed = serverMoment.diff(moment(entityConfig.lastChangedTime), 'milliseconds');
                if (elapsed < 0) {
                  this.setTransitionFill(svgElement, fromFill, toFill, 1);
                }
                else {
                  if (elapsed < (transitionConfig.duration * 1000)) {
                    this.setTransitionFill(svgElement, fromFill, toFill, elapsed / (transitionConfig.duration * 1000));
                  }
                  else {
                    this.setTransitionFill(svgElement, fromFill, toFill, 0);
                    entityConfig.lastChangedTime = undefined;
                  }
                }

                wasTransitionHandled = true;
              }
            }
          }

          if (!wasTransitionHandled) {
            this.setEntityStyle(svgElementConfig, svgElement, entityConfig);
          }
        }
      }
    }

    setEntityStyle(svgElementConfig, svgElement, entityConfig, state) {
      let stateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === entityConfig.lastState));
      if (stateConfig) {
        let stroke = this.getStroke(stateConfig);
        if (stroke) {
          svgElement.style.stroke = stroke;
        }
        else {
          if (svgElementConfig.clonedsvgElement) {
            svgElement.style.stroke = svgElementConfig.clonedsvgElement.style.stroke;
          }
          else {
            // ???
          }
        }

        let fill = this.getFill(stateConfig);
        if (fill) {
          svgElement.style.fill = fill;
        }
        else {
          if (svgElementConfig.clonedsvgElement) {
            svgElement.style.fill = svgElementConfig.clonedsvgElement.style.fill;
          }
          else {
            // ???
          }
        }
      }
    }

    onElementClick(e) {
      e.stopPropagation();

      let svgElement = e.target;

      let elementConfig = this.instance.elementConfigs[this.elementId];
      if (elementConfig.group.action) {
        let action = elementConfig.group.action;
        if (action.service) {
          switch (action.domain) {
            case 'class':

              switch (action.service) {
                case 'toggle':
                  let svg = this.instance.$.floorplan.querySelector('svg');
                  let classes = action.data.classes;

                  for (let otherElementId of action.data.elements) {
                    let otherSvgElement = $(svg).find(`[id="${otherElementId}"]`);

                    if ($(otherSvgElement).hasClass(classes[0])) {
                      $(otherSvgElement).removeClass(classes[0]);
                      $(otherSvgElement).addClass(classes[1]);
                    }
                    else if ($(otherSvgElement).hasClass(classes[1])) {
                      $(otherSvgElement).removeClass(classes[1]);
                      $(otherSvgElement).addClass(classes[0]);
                    }
                    else {
                      $(otherSvgElement).addClass(action.data.default_class);
                    }
                  }
                  break;
              }
              break;

            default:
              domain = action.domain
              let data = action.data ? action.data : {};
              if (action.data_template) {
                let entities = this.instance.hass.states;
                let entityState = entities[entityId];
                let result = this.instance.assemble(action.data_template, entityState, entities);
                data = JSON.parse(result);
              }
              this.instance.hass.callService(domain, action.service, data);
              break;
          }
        }
      }
    }

    onEntityClick(e) {
      e.stopPropagation();

      let entityId = this.entityId;

      let entityConfig = this.instance.entityConfigs[entityId];
      if (entityConfig.group.action) {
        let action = entityConfig.group.action;
        if (action.service) {
          let domain = action.domain ? action.domain : entityId.substr(0, entityId.indexOf('.'));
          domain = (domain == 'group') ? 'homeassistant' : domain;

          let data = {};
          if (action.data) {
            data = action.data;
          }
          if (action.data_template) {
            let entities = this.instance.hass.states;
            let entityState = entities[entityId];

            let result = this.instance.assemble(action.data_template, entityState, entities);
            data = JSON.parse(result);
          }

          if (!data.entity_id) {
            data['entity_id'] = entityId;
          }

          this.instance.hass.callService(domain, action.service, data);
        }
        else {
          this.instance.fire('hass-more-info', { entityId: entityId });
        }
      }
      else {
        this.instance.fire('hass-more-info', { entityId: entityId });
      }
    }

    getFill(stateConfig) {
      let fill = undefined;

      for (let cssRule of this.cssRules) {
        if (cssRule.selectorText && cssRule.selectorText.indexOf(`.${stateConfig.class}`) >= 0) {
          if (cssRule.style && cssRule.style.fill) {
            if (cssRule.style.fill[0] === '#') {
              fill = cssRule.style.fill;
            }
            else {
              let rgb = cssRule.style.fill.substring(4).slice(0, -1).split(',').map(x => parseInt(x));
              fill = `#${rgb[0].toString(16)[0]}${rgb[1].toString(16)[0]}${rgb[2].toString(16)[0]}`;
            }
          }
        }
      }

      return fill;
    }

    getStroke(stateConfig) {
      let stroke = undefined;

      for (let cssRule of this.cssRules) {
        if (cssRule.selectorText && cssRule.selectorText.indexOf(`.${stateConfig.class}`) >= 0) {
          if (cssRule.style && cssRule.style.stroke) {
            if (cssRule.style.stroke[0] === '#') {
              stroke = cssRule.style.stroke;
            }
            else {
              let rgb = cssRule.style.stroke.substring(4).slice(0, -1).split(',').map(x => parseInt(x));
              stroke = `#${rgb[0].toString(16)[0]}${rgb[1].toString(16)[0]}${rgb[2].toString(16)[0]}`;
            }
          }
        }
      }

      return stroke;
    }

    setTransitionFill(svgElement, fromFill, toFill, value) {
      if (value >= 1) {
        svgElement.style.fill = fromFill;
      }
      else if (value <= 0) {
        svgElement.style.fill = toFill;
      }
      else {
        let color = this.rgbToHex(this.mix(this.hexToRgb(toFill), this.hexToRgb(fromFill), value));
        svgElement.style.fill = color;
      }
    }

    getServerMoment() {
      let serverMoment = moment();
      if (this.timeDifference >= 0)
        serverMoment.subtract(this.timeDifference, 'milliseconds');
      else
        serverMoment.add(Math.abs(this.timeDifference), 'milliseconds');
      return serverMoment;
    }

    getArray(list) {
      return Array.isArray(list) ? list : Object.keys(list).map(key => list[key]);
    }

    assemble(code, entity, entities) {
      let functionBody = (code.indexOf('return') >= 0) ? code : `return \`${code}\`;`;
      let func = new Function('entity', 'entities', 'hass', 'config', functionBody);
      return func(entity, entities, this.hass, this.config);
    }

    error(message) {
      let errors = this.$.errors;
      $(errors).find('ul').append(`<li>${message}</li>`)
      $(errors).css('display', 'block');
    }

    warn(message) {
      if ((this.config.warnings === null) || (this.config.warnings !== undefined)) {
        let warnings = this.$.warnings;
        $(warnings).find('ul').append(`<li>${message}</li>`)
        $(warnings).css('display', 'block');
      }
    }

    debug(message) {
      let debug = this.$.debug;
      $(debug).find('ul').append(`<li>${message}</li>`)
      $(debug).css('display', 'block');
    }

    loadStyleSheet(path, fn, scope) {
      let head = document.getElementsByTagName('head')[0]; // reference to document.head for appending/ removing link nodes
      let link = document.createElement('link');           // create the link node
      link.setAttribute('href', path);
      link.setAttribute('rel', 'stylesheet');
      link.setAttribute('type', 'text/css');

      let sheet, cssRules;
      // get the correct properties to check for depending on the browser
      if ('sheet' in link) {
        sheet = 'sheet'; cssRules = 'cssRules';
      }
      else {
        sheet = 'styleSheet'; cssRules = 'rules';
      }

      let interval_id = setInterval(function () {                     // start checking whether the style sheet has successfully loaded
        try {
          if (link[sheet] && link[sheet][cssRules].length) { // SUCCESS! our style sheet has loaded
            clearInterval(interval_id);                      // clear the counters
            clearTimeout(timeout_id);
            fn.call(scope || window, true, link);           // fire the callback with success == true
          }
        } catch (e) { } finally { }
      }, 10),                                                   // how often to check if the stylesheet is loaded
        timeout_id = setTimeout(function () {       // start counting down till fail
          clearInterval(interval_id);             // clear the counters
          clearTimeout(timeout_id);
          head.removeChild(link);                // since the style sheet didn't load, remove the link node from the DOM
          fn.call(scope || window, false, link); // fire the callback with success == false
        }, 15000);                                 // how long to wait before failing

      head.appendChild(link);  // insert the link node into the DOM and start loading the style sheet

      return link; // return the link node;
    }

    rgbToHex(rgb) {
      return "#" + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1);
    }

    hexToRgb(hex) {
      // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
      let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
      hex = hex.replace(shorthandRegex, (m, r, g, b) => {
        return r + r + g + g + b + b;
      });

      let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    }

    mix(color1, color2, weight) {
      let p = weight;
      let w = p * 2 - 1;
      let w1 = ((w / 1) + 1) / 2;
      let w2 = 1 - w1;
      let rgb = [
        Math.round(color1.r * w1 + color2.r * w2),
        Math.round(color1.g * w1 + color2.g * w2),
        Math.round(color1.b * w1 + color2.b * w2)
      ];
      return rgb;
    }

    fire(type, detail, options) {
      options = options || {};
      detail = (detail === null || detail === undefined) ? {} : detail;
      const event = new Event(type, {
        bubbles: options.bubbles === undefined ? true : options.bubbles,
        cancelable: Boolean(options.cancelable),
        composed: options.composed === undefined ? true : options.composed
      });
      event.detail = detail;
      const node = options.node || this;
      node.dispatchEvent(event);
      return event;
    }
  }
  customElements.define(HaFloorplan.is, HaFloorplan);

</script>
